<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"><meta name="description" content=""><meta name="keywords" content=""><meta name="author" content="LiYang"><meta name="copyright" content="LiYang"><title>一条鲤鱼</title><link rel="shortcut icon" href="/melody-favicon.ico"><link rel="stylesheet" href="/css/index.css?version=1.9.0"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@latest/css/font-awesome.min.css?version=1.9.0"><meta name="format-detection" content="telephone=no"><meta http-equiv="x-dns-prefetch-control" content="on"><link rel="dns-prefetch" href="https://cdn.jsdelivr.net"><meta http-equiv="Cache-Control" content="no-transform"><meta http-equiv="Cache-Control" content="no-siteapp"><script>var GLOBAL_CONFIG = { 
  root: '/',
  algolia: undefined,
  localSearch: undefined,
  copy: {
    success: 'Copy successfully',
    error: 'Copy error',
    noSupport: 'The browser does not support'
  },
  hexoVersion: '5.4.0'
} </script><meta name="generator" content="Hexo 5.4.0"><link rel="alternate" href="/atom.xml" title="一条鲤鱼" type="application/atom+xml">
</head><body><i class="fa fa-arrow-right" id="toggle-sidebar" aria-hidden="true"></i><div id="sidebar" data-display="false"><div class="author-info"><div class="author-info__avatar text-center"><img src="/img/avatar.png"></div><div class="author-info__name text-center">LiYang</div><div class="author-info__description text-center"></div><hr><div class="author-info-articles"><a class="author-info-articles__archives article-meta" href="/archives"><span class="pull-left">Articles</span><span class="pull-right">13</span></a><a class="author-info-articles__tags article-meta" href="/tags"><span class="pull-left">Tags</span><span class="pull-right">6</span></a><a class="author-info-articles__categories article-meta" href="/categories"><span class="pull-left">Categories</span><span class="pull-right">7</span></a></div></div></div><nav class="no-bg" id="nav"><div id="page-header"><span class="pull-left"> <a id="site-name" href="/">一条鲤鱼</a></span><i class="fa fa-bars toggle-menu pull-right" aria-hidden="true"></i><span class="pull-right menus">   <a class="site-page" href="/">Home</a><a class="site-page" href="/archives">Archives</a><a class="site-page" href="/tags">Tags</a><a class="site-page" href="/categories">Categories</a></span><span class="pull-right"></span></div><div id="site-info"><div id="site-title">一条鲤鱼</div><div id="site-sub-title"></div></div></nav><div id="content-outer"><div class="layout" id="content-inner"><div class="recent-post-item article-container"><a class="article-title" href="/2021/09/22/%E7%90%86%E8%A7%A3Rust%E7%9A%84%E5%BC%95%E7%94%A8%E5%92%8C%E5%80%9F%E7%94%A8/">理解Rust的引用和借用</a><time class="post-meta__date"><i class="fa fa-calendar" aria-hidden="true"></i> 2021-09-22</time><span class="article-meta"><span class="article-meta__separator">|</span><i class="fa fa-inbox article-meta__icon" aria-hidden="true"></i><a class="article-meta__categories" href="/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/">编程语言</a></span><span class="article-meta tags"><span class="article-meta__separator">|</span><i class="fa fa-tag article-meta__icon" aria-hidden="true"></i><a class="article-meta__tags" href="/tags/Rust/">Rust</a></span><div class="content"><p>​    学习了一段时间的Rust编程语言后，对Rust中的<strong>引用（References）</strong>和<strong>借用（borrowing）</strong>产生了一些疑问：</p>
<ul>
<li>1、对比C++中的引用，rust中的引用是否也是变量别名，作为变量附属在内存位置中的第二个标签？</li>
<li>2、&amp;关键字是代表引用还是借用？</li>
<li>3、ref和&amp;有什么区别？</li>
</ul>
<hr>
<p>​    看了几本书中的介绍，对rust中的引用和借用描述的云里雾里的，比如<strong>《Rust编程 入门、实战与进阶》</strong>一书中对引用和借用的介绍如下：引用是一种语法（本质上是Rust提供的一种指针语义），而借用（Borrowing）是对引用行为的描述。引用分为不可变引用和可变引用，对应着不可变借用和可变借用。</p>
<p>​    这样说完，是不是更晕了呢。不如我们从代码入手，分析一下到底什么是Rust中的引用，什么是Rust中的借用。这里笔者说下自己的理解。</p>
<hr>
<h2 id="什么是借用？"><a href="#什么是借用？" class="headerlink" title="什么是借用？"></a>什么是借用？</h2><p>根据笔者看过的基本Rust编程语言的教学入门书籍，都是从Rust较与其他语言所提出的新概念：借用 来介绍的。那我们不妨从为什么借用开始：</p>
<p>​    首先，看下面这段代码：</p>
<figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fn</span> <span class="title">main</span></span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> s1 = <span class="built_in">String</span>::from(<span class="string">&quot;hello&quot;</span>);</span><br><span class="line">    <span class="keyword">let</span> len = calculate_length(&amp;s1);</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;The length of &#x27;&#123;&#125;&#x27; is &#123;&#125;.&quot;</span>, s1, len);</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">fn</span> <span class="title">calculate_length</span></span>(s: &amp;<span class="built_in">String</span>) -&gt; <span class="built_in">usize</span> &#123;<span class="comment">// s 是对 String 的借用</span></span><br><span class="line">    s.len()</span><br><span class="line">&#125;<span class="comment">// 这里，s 离开了作用域。但因为它并不拥有借用值的所有权，</span></span><br><span class="line">  <span class="comment">// 所以什么也不会发生</span></span><br></pre></td></tr></table></figure>

<ul>
<li>这里，我们需要使用借用，因为我们要在不获得s1的所有权的情况下使用s1的值，这就是“借（borrow）“的含义。**&amp; 符号就是借用，它们允许你使用值但不获取其所有权。**</li>
<li>从内存管理来看，这段代码在内存上做的工作如下图所示：</li>
</ul>
<p><img src="/2021/09/22/%E7%90%86%E8%A7%A3Rust%E7%9A%84%E5%BC%95%E7%94%A8%E5%92%8C%E5%80%9F%E7%94%A8/1.1.png" alt="1.1"></p>
<p>​                                            <em>图1.1：<code>&amp;String s</code> 指向 <code>String s1</code> 示意图</em></p>
<ul>
<li>根据示意图，我们知道，所谓借用，就是将一个指针的值域指向需要被借用的变量，这样就能够在不取走原本变量对值的所有权的情况下，使用其所有的值。这样说，听起来有些绕，但是这就是Rust所有权系统的含义之一，与<strong>生命周期</strong>结合起来理解，<code>&amp;s1</code> 语法让我们创建一个 <strong>指向</strong> 值 <code>s1</code> 的引用，但是并不拥有它。因为并不拥有这个值，当引用离开作用域时其指向的值也不会被丢弃。</li>
<li>借用默认是只读的，想要修改借用的值，可以使用<strong>可变引用&amp;mut</strong>，这里就不展开讲了。</li>
<li>另外，与使用 <code>&amp;</code> 借用相反的操作是 <strong>解引用</strong>（<em>dereferencing</em>），它使用解引用运算符，<code>*</code>。</li>
</ul>
<h2 id="引用和借用"><a href="#引用和借用" class="headerlink" title="引用和借用"></a>引用和借用</h2><p>​    简单介绍了Rust中的借用，我们发现，借用的意义确实像是C++等语言中的引用，是用来间接访问对象的一种手段。那么什么是Rust中的引用呢？</p>
<ul>
<li><p><strong>rust中借用和引用的附带功效都一样，就是都有生命周期。借用使用&amp;关键字，引用使用ref关键字。借用的对象是必须存在的，引用的对象可以虚拟的，后期附上对象。</strong>这也是与c++语言中引用不同的地方，c++ 语言说的是，引用类似变量别名，声明引用时，必须同时对其进行初始化。</p>
</li>
<li><p>再看下出自《Programming Rust Fast, Safe Systems Development》</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">in Rust terminology, we say that it borrows a reference to x. Given a</span><br><span class="line">reference r, the expression *r refers to the value r points to. These are very much like</span><br><span class="line">the &amp; and * operators in C and C++</span><br><span class="line">Unlike C pointers, however, Rust references are never null: there is simply no way to</span><br><span class="line">produce a null reference in safe Rust. And Rust references are immutable by default</span><br></pre></td></tr></table></figure>

<p>书中将rust的引用与c++的指针进行了比较。</p>
</li>
<li><p>我们还是从代码入手，看一下rust语言中引用到底是什么：</p>
<ul>
<li><p>首先实践，引用能否为空：</p>
<figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="keyword">ref</span> a:<span class="built_in">i32</span>;</span><br><span class="line">a = &amp;<span class="number">1</span>;</span><br></pre></td></tr></table></figure></li>
<li><p>上面代码能编译通过，<strong>这说明Rust中的引用和c++ 中的引用不是一个意思，更像c++ 中的指针类型</strong>。c++ 通过<code>int *a</code>来声明指针类型。</p>
</li>
</ul>
</li>
<li><p>再来分析一下引用（ref）和借用（&amp;）的区别：</p>
<ul>
<li>```rust<br>let ref a=2;//&amp;i32<br>let a = &2;//&amp;i32<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">  * 编译器告诉我们，两个值都是`&amp;i32`类型。</span><br><span class="line"></span><br><span class="line">  * 所以`ref`用在变量绑定与`&amp;`用在表达式上是一样的效果。</span><br><span class="line"></span><br><span class="line">* 加深一下理解，我们直接通过代码输出告诉我们&amp;和ref的类型是什么：</span><br><span class="line"></span><br><span class="line">  ```rust</span><br><span class="line">  fn main() &#123;</span><br><span class="line">      let x = &amp;false;</span><br><span class="line">      print_type_name_of(x);</span><br><span class="line">  </span><br><span class="line">      let &amp;y = &amp;false;</span><br><span class="line">      print_type_name_of(y);</span><br><span class="line">  </span><br><span class="line">      let ref z = &amp;false;</span><br><span class="line">      print_type_name_of(z);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  fn print_type_name_of&lt;T&gt;(_: T) &#123;</span><br><span class="line">      println!(&quot;&#123;&#125;&quot;, unsafe &#123; std::intrinsics::type_name::&lt;T&gt;() &#125;)</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure></li>
</ul>
</li>
<li><p>通过输出结果，我们知道了x是&amp;bool类型，y是bool类型，z是&amp;&amp;bool类型。</p>
</li>
</ul>
<p>除此之外，Rust中的引用和借用还有一个要注意的地方：<strong>match的模式匹配上只能使用 <code>ref</code>，在函数声明上只能使用<code>&amp;</code>来表示引用类型。</strong></p>
<hr>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><strong>Rust中&amp;在不同情况下的意思：</strong></p>
<ol>
<li>在表达式上，表示的是借用。</li>
<li>在变量绑定上，表示解地址操作与*类似。</li>
<li>在类型声明上，表示引用类型。</li>
<li>在模式匹配上，<strong>无效关键字，应使用ref。</strong></li>
</ol>
<p><strong>Rust中ref的解释：</strong></p>
<ol>
<li>在表达式上，<strong>无效关键字</strong>。</li>
<li>在变量绑定上，表示引用类型。</li>
<li>在类型声明上，<strong>无效关键字</strong>。</li>
<li>在模式匹配上，表示引用类型。</li>
</ol>
<p>​    如果非要给区分<code>ref</code>和<code>&amp;</code>到底哪个是引用，哪个是借用。我们可以先从词性划分，引用我归类为名词，而借用归类为动词。<code>&amp;A</code>在表达式上 表示借用A，这是一个动作，那结果就是产出一个引用类型。所以<code>let ref B</code>表示声明了一个引用类型，它只能绑定到某次借用动作上。</p>
<p><strong>所以<code>ref</code> 更适合叫引用， <code>&amp;</code>叫借用。</strong></p>
</div><hr></div><div class="recent-post-item article-container"><a class="article-title" href="/2021/09/15/Rust%E4%B8%AD%E7%9A%84Rc%E5%92%8CBox-leak-%E6%9C%BA%E5%88%B6/">Rust中的Rc、Arc和Box::leak()机制</a><time class="post-meta__date"><i class="fa fa-calendar" aria-hidden="true"></i> 2021-09-15</time><span class="article-meta"><span class="article-meta__separator">|</span><i class="fa fa-inbox article-meta__icon" aria-hidden="true"></i><a class="article-meta__categories" href="/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/">编程语言</a></span><span class="article-meta tags"><span class="article-meta__separator">|</span><i class="fa fa-tag article-meta__icon" aria-hidden="true"></i><a class="article-meta__tags" href="/tags/Rust/">Rust</a></span><div class="content"><p>​    单一所有权规则，能满足我们大部分场景中分配和使用内存的需求，而且在编译时，通过 Rust 借用检查器就能完成静态检查，不会影响运行时效率。</p>
<p>​    但是，规则总会有例外，在日常工作中有些特殊情况该怎么处理呢？</p>
<ul>
<li>一个有向无环图（DAG）中，某个节点可能有两个以上的节点指向它，这个按照所有权模型怎么表述？</li>
<li>多个线程要访问同一块共享内存，怎么办？</li>
</ul>
<p>​    我们知道，这些问题在程序运行过程中才会遇到，在编译期，所有权的静态检查无法处理它们，所以为了更好的灵活性，Rust 提供了<strong>运行时的动态检查</strong>，来满足特殊场景下的需求。</p>
<p>​    这也是 Rust 处理很多问题的思路：<strong>编译时，处理大部分使用场景，保证安全性和效率；运行时，处理无法在编译时处理的场景，会牺牲一部分效率，提高灵活性。</strong></p>
<p>​    那么具体如何在运行时做动态检查呢？运行时的动态检查又如何与编译时的静态检查自洽呢？</p>
<p>​    Rust 的答案是使用引用计数的智能指针：<strong>Rc（Reference counter） 和 Arc（Atomic reference counter）</strong>。这里要特别说明一下，Arc 和 ObjC/Swift 里的 ARC（Automatic Reference Counting）不是一个意思，不过它们解决问题的手段类似，都是通过引用计数完成的。</p>
<h2 id="Rc"><a href="#Rc" class="headerlink" title="Rc"></a>Rc</h2><p>​    首先介绍Rc。对某个数据结构 T，我们可以创建引用计数 Rc，使其有多个所有者。Rc 会把对应的数据结构创建在堆上，堆是唯一可以让动态创建的数据被到处使用的内存。</p>
<figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">use</span> std::rc::Rc;</span><br><span class="line"><span class="function"><span class="keyword">fn</span> <span class="title">main</span></span>() &#123;    </span><br><span class="line">  <span class="keyword">let</span> a = Rc::new(<span class="number">1</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>之后，如果想对数据创建更多的所有者，我们可以通过 <strong>clone()</strong> 来完成。</p>
<p><strong>对一个 Rc 结构进行 clone()，不会将其内部的数据复制，只会增加引用计数。</strong>而当一个 Rc 结构离开作用域被 drop() 时，也只会减少其引用计数，直到引用计数为零，才会真正清除对应的内存。</p>
<figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">use</span> std::rc::Rc;</span><br><span class="line"><span class="function"><span class="keyword">fn</span> <span class="title">main</span></span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> a = Rc::new(<span class="number">1</span>);</span><br><span class="line">    <span class="keyword">let</span> b = a.clone();</span><br><span class="line">    <span class="keyword">let</span> c = a.clone();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>上面的代码我们创建了三个 Rc，分别是 a、b 和 c。它们共同指向堆上相同的数据，也就是说，堆上的数据有了三个共享的所有者。在这段代码结束时，c 先 drop，引用计数变成 2，然后 b drop、a drop，引用计数归零，堆上内存被释放。</p>
<p><img src="/2021/09/15/Rust%E4%B8%AD%E7%9A%84Rc%E5%92%8CBox-leak-%E6%9C%BA%E5%88%B6/a3510f9b565577bc74bc0dcda0b3e78c.png" alt="a3510f9b565577bc74bc0dcda0b3e78c"></p>
<p>你也许会有疑问：为什么我们生成了对同一块内存的多个所有者，但是，编译器不抱怨所有权冲突呢？</p>
<p>仔细看这段代码：首先 a 是 Rc::new(1) 的所有者，这毋庸置疑；然后 b 和 c 都调用了 a.clone()，分别得到了一个新的 Rc，所以从编译器的角度，abc 都各自拥有一个 Rc。如果文字你觉得稍微有点绕，可以看看 Rc 的 clone() 函数的实现，就很清楚了（<a target="_blank" rel="noopener" href="https://doc.rust-lang.org/src/alloc/rc.rs.html#1433-1453">源代码</a>）：</p>
<figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fn</span> <span class="title">clone</span></span>(&amp;<span class="keyword">self</span>) -&gt; Rc&lt;T&gt; &#123;</span><br><span class="line">    <span class="comment">// 增加引用计数</span></span><br><span class="line">    <span class="keyword">self</span>.inner().inc_strong();</span><br><span class="line">    <span class="comment">// 通过 self.ptr 生成一个新的 Rc 结构</span></span><br><span class="line">    Self::from_inner(<span class="keyword">self</span>.ptr)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>所以，Rc 的 clone() 正如我们刚才说的，不复制实际的数据，只是一个引用计数的增加。你可能继续会疑惑：Rc 是怎么产生在堆上的？并且为什么这段堆内存不受栈内存生命周期的控制呢？</p>
<p>这就要说到Box:leak（）机制了。</p>
<h2 id="Box-leak-机制"><a href="#Box-leak-机制" class="headerlink" title="Box::leak()机制"></a>Box::leak()机制</h2><p>在所有权模型下，堆内存的生命周期，和创建它的栈内存的生命周期保持一致。所以 Rc 的实现似乎与此格格不入。的确，如果完全按照单一所有权模型，Rust 是无法处理 Rc 这样的引用计数的。</p>
<p>Rust 必须提供一种机制，让代码可以像 C/C++ 那样，<strong>创建不受栈内存控制的堆内存，从而绕过编译时的所有权规则。Rust 提供的方式是 Box::leak()。</strong></p>
<p>Box 是 Rust 下的智能指针，它可以强制把任何数据结构创建在堆上，然后在栈上放一个指针指向这个数据结构，但此时堆内存的生命周期仍然是受控的，跟栈上的指针一致。</p>
<p>而<strong>Box::leak()<strong>，顾名思义，它创建的对象，</strong>从堆内存上泄漏出去，不受栈内存控制，是一个自由的、生命周期可以大到和整个进程的生命周期一致的对象。</strong></p>
<p>下面这张图更容易理解：</p>
<p><img src="/2021/09/15/Rust%E4%B8%AD%E7%9A%84Rc%E5%92%8CBox-leak-%E6%9C%BA%E5%88%B6/9f1a17dea75f9cae596a56f51d007ccd.png" alt="9f1a17dea75f9cae596a56f51d007ccd"></p>
<p>Box::leak()相当于主动撕开了一个口子，允许内存泄漏。注意，<strong>在 C/C++ 下，其实你通过 malloc 分配的每一片堆内存，都类似 Rust 下的 Box::leak()。</strong> Rust 这样的设计符合最小权限原则（<a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Principle_of_least_privilege">Principle of least privilege</a>），可以最大程度帮助开发者撰写安全的代码。</p>
<p>有了 Box::leak()，我们就可以跳出 Rust 编译器的静态检查，保证 Rc 指向的堆内存，有最大的生命周期，然后我们再通过引用计数，在合适的时机，结束这段内存的生命周期。如果你对此感兴趣，可以看 <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/src/alloc/rc.rs.html#342-350">Rc::new() 的源码</a>。</p>
<p>搞明白了 Rc，我们就进一步理解 Rust 是如何进行所有权的静态检查和动态检查了：</p>
<ul>
<li>静态检查，靠编译器保证代码符合所有权规则；</li>
<li>动态检查，通过 Box::leak 让堆内存拥有不受限的生命周期，然后在运行过程中，通过对引用计数的检查，保证这样的堆内存最终会得到释放。</li>
</ul>
<h3 id="可变的“Rc”——RefCell"><a href="#可变的“Rc”——RefCell" class="headerlink" title="可变的“Rc”——RefCell"></a>可变的“Rc”——RefCell</h3><p>Rc 是一个只读的引用计数器，你无法拿到 Rc 结构内部数据的可变引用，来修改这个数据。这就需要RefCell了。</p>
<p>和 Rc 类似，RefCell 也绕过了 Rust 编译器的静态检查，允许我们在运行时，对某个只读数据进行可变借用。这就涉及 Rust 另一个比较独特且有点难懂的概念：<a target="_blank" rel="noopener" href="https://doc.rust-lang.org/book/ch15-05-interior-mutability.html">内部可变性（interior mutability）</a>。</p>
<h4 id="内部可变性"><a href="#内部可变性" class="headerlink" title="内部可变性"></a>内部可变性</h4><p>首先，我们已经熟悉了外部可变性。当我们用 let mut 显式地声明一个可变的值，或者，用 &amp;mut 声明一个可变引用时，编译器可以在编译时进行严格地检查，保证只有可变的值或者可变的引用，才能修改值内部的数据，这被称作<strong>外部可变性（exterior mutability）</strong>，外部可变性通过 mut 关键字声明。</p>
<p>然而，这样不够灵活，有时候我们希望能够绕开这个编译时的检查，对并未声明成 mut 的值或者引用，也想进行修改。也就是说，<strong>在编译器的眼里，值是只读的，但是在运行时，这个值可以得到可变借用，从而修改内部的数据，这就是 RefCell 的用武之地。</strong></p>
<p>看一个简单的例子：</p>
<figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">use</span> std::cell::RefCell;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fn</span> <span class="title">main</span></span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> data = RefCell::new(<span class="number">1</span>);</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// 获得 RefCell 内部数据的可变借用</span></span><br><span class="line">        <span class="keyword">let</span> <span class="keyword">mut</span> v = data.borrow_mut();</span><br><span class="line">        *v += <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;data: &#123;:?&#125;&quot;</span>, data.borrow());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在这个例子里，data 是一个 RefCell，其初始值为 1。可以看到，我们并未将 data 声明为可变变量。之后我们可以通过使用 RefCell 的 <strong>borrow_mut() 方法</strong>，来<strong>获得一个可变的内部引用</strong>，然后对它做加 1 的操作。最后，我们可以通过 <strong>RefCell 的 borrow() 方法</strong>，<strong>获得一个不可变的内部引用</strong>，因为加了 1，此时它的值为 2。</p>
<p><strong>注意</strong>：这里为什么要把获取和操作可变借用的两句代码，用花括号分装到一个作用域下？</p>
<p>因为根据所有权规则，在同一个作用域下，<strong>我们不能同时有活跃的可变借用和不可变借用</strong>。通过这对花括号，我们明确地缩小了可变借用的生命周期，不至于和后续的不可变借用冲突。</p>
<p><strong>外部可变性和内部可变性的重要区别</strong>用下表来总结一下：</p>
<p><img src="/2021/09/15/Rust%E4%B8%AD%E7%9A%84Rc%E5%92%8CBox-leak-%E6%9C%BA%E5%88%B6/94bd27a93210ea829482663c9138de3c.png" alt="94bd27a93210ea829482663c9138de3c"></p>
<h2 id="Arc和Mutex-RwLock"><a href="#Arc和Mutex-RwLock" class="headerlink" title="Arc和Mutex/RwLock"></a>Arc和Mutex/RwLock</h2><p>我们可以用 Rc 和 RefCell 解决 DAG 的问题，那么，多个线程访问同一块内存的问题，是否也可以使用 Rc 来处理呢？</p>
<p>不行。因为 <strong>Rc 为了性能，使用的不是线程安全的引用计数器</strong>。因此，我们需要另一个引用计数的智能指针：<strong>Arc，它实现了线程安全的引用计数器。</strong></p>
<p>Arc 内部的引用计数使用了 <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/src/alloc/sync.rs.html#303-312">Atomic Usize</a> ，而非普通的 usize。从名称上也可以感觉出来，Atomic Usize 是 usize 的原子类型，它使用了 CPU 的特殊指令，来保证多线程下的安全。如果你对原子类型感兴趣，可以看 <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/sync/atomic/index.html">std::sync::atomic 的文档</a>。</p>
<p>Rust 实现两套不同的引用计数数据结构，完全是为了性能考虑，从这里我们也可以感受到 Rust 对性能的极致渴求。<strong>如果不用跨线程访问，可以用效率非常高的 Rc；如果要跨线程访问，那么必须用 Arc</strong>。</p>
<p>同样的，<strong>RefCell 也不是线程安全的</strong>，如果我们要在多线程中，使用内部可变性，Rust 提供了 <strong>Mutex 和 RwLock</strong>。</p>
<p><strong>Mutex 是互斥量</strong>，获得互斥量的线程对数据独占访问，<strong>RwLock 是读写锁</strong>，获得写锁的线程对数据独占访问，但当没有写锁的时候，允许有多个读锁。读写锁的规则和 Rust 的借用规则非常类似，我们可以类比着学。</p>
<p>Mutex 和 RwLock 都用在多线程环境下，对共享数据访问的保护上。多线程中需要把  Rc&lt;RefCell<T>&gt; 替换为  Arc&lt;Mutex<T>&gt; 或者  Arc&lt;RwLock<T>&gt;。</T></T></T></p>
<h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><ul>
<li><p>我们对所有权有了更深入的了解，掌握了 <strong>Rc / Arc、RefCell / Mutex / RwLock</strong> 这些数据结构的用法。</p>
</li>
<li><p>如果想绕过“一个值只有一个所有者”的限制，我们可以使用 <strong>Rc / Arc 这样带引用计数的智能指针</strong>。其中，Rc 效率很高，但只能使用在单线程环境下；Arc 使用了原子结构，效率略低，但可以安全使用在多线程环境下。</p>
</li>
<li><p>然而，Rc / Arc 是不可变的，如果想要修改内部的数据，需要引入<strong>内部可变性</strong>，在单线程环境下，可以在 Rc 内部使用 <strong>RefCell</strong>；在多线程环境下，可以使用 Arc 嵌套 <strong>Mutex</strong> 或者 <strong>RwLock</strong> 的方法。</p>
</li>
</ul>
<p><img src="/2021/09/15/Rust%E4%B8%AD%E7%9A%84Rc%E5%92%8CBox-leak-%E6%9C%BA%E5%88%B6/fc524d667fabeec0a8a22d0e10531086.png" alt="fc524d667fabeec0a8a22d0e10531086"></p>
</div><hr></div><div class="recent-post-item article-container"><a class="article-title" href="/2021/09/14/learnMPI/">MPI</a><time class="post-meta__date"><i class="fa fa-calendar" aria-hidden="true"></i> 2021-09-14</time><span class="article-meta"><span class="article-meta__separator">|</span><i class="fa fa-inbox article-meta__icon" aria-hidden="true"></i><a class="article-meta__categories" href="/categories/%E5%B9%B6%E8%A1%8C%E8%AE%A1%E7%AE%97/">并行计算</a></span><span class="article-meta tags"><span class="article-meta__separator">|</span><i class="fa fa-tag article-meta__icon" aria-hidden="true"></i><a class="article-meta__tags" href="/tags/MPI/">MPI</a></span><div class="content"><h1 id="并行程序设计导论"><a href="#并行程序设计导论" class="headerlink" title="并行程序设计导论"></a>并行程序设计导论</h1><h2 id="用MPI进行分布式内存编程"><a href="#用MPI进行分布式内存编程" class="headerlink" title="用MPI进行分布式内存编程"></a>用MPI进行分布式内存编程</h2><p>​    在消息传递程序中，运行在一个核-内存对上的程序通常称为一个<strong>进程</strong>。两个进程可以通过调用函数来进行通信：一个进程调用<strong>发送</strong>函数，另一个调用<strong>接收</strong>函数。我们将使用消息传递的实现称为消息传递接口（Message-Passing Interface，MPI）。</p>
<p>​    MPI并不是一种新的语言，它定义了一个可以被C、C++和Fortran程序调用的函数库。我们将介绍MPI中的一些不同的发送与接收函数。还将学习一些可以涉及多于两个进程的“全局”通信函数。</p>
<h3 id="1、预备知识"><a href="#1、预备知识" class="headerlink" title="1、预备知识"></a>1、预备知识</h3><h4 id="1-1-编译与执行"><a href="#1-1-编译与执行" class="headerlink" title="1.1 编译与执行"></a>1.1 编译与执行</h4><p>​    编译与运行程序主要取决于系统。许多系统都有称为mpicc的命令来编译程序：</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> mpicc -g -Wall -o mpi_hello mpi_hello.c</span></span><br></pre></td></tr></table></figure>

<p><em>注：-g表示允许使用调试器         -Wall表示显式警告           -o<outfile>编译出的可执行文件的文件名为outfile</outfile></em></p>
<p><em>在对程序计时时，我们用-O2选项来告诉编译器对代码进行优化。</em></p>
<ul>
<li><p>典型情况下，mpiicc是C语言编译器的<strong>包装脚本</strong>（wrapper script）。包装脚本的主要目的是运行某个程序。在这种情况下，程序就是C语言编译器。通过告知编译器从何处取得需要的头文件、什么库函数连接到对象文件等，包装脚本可以简化编译器的运行。</p>
</li>
<li><p>许多系统还支持用mpiexec命令来启动程序：</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> mpiexec -n &lt;number of processes&gt; ./mpi_hello</span></span><br></pre></td></tr></table></figure></li>
</ul>
<p><strong>一个简单例子：打印来自进程问候语句的MPI程序</strong></p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span><span class="meta-string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span><span class="meta-string">&lt;string.h&gt;</span> </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span><span class="meta-string">&lt;mpi.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="keyword">int</span> MAX_STRING=<span class="number">100</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">void</span>)</span></span>&#123;</span><br><span class="line">    <span class="keyword">char</span> greeting[MAX_STRING];</span><br><span class="line">    <span class="keyword">int</span> comm_sz;<span class="comment">//进程数量</span></span><br><span class="line">    <span class="keyword">int</span> my_rank; <span class="comment">//进程号</span></span><br><span class="line"></span><br><span class="line">    MPI_Init(<span class="literal">NULL</span>,<span class="literal">NULL</span>);</span><br><span class="line">    MPI_Comm_size(MPI_COMM_WORLD,&amp;comm_sz);</span><br><span class="line">    MPI_Comm_rank(MPI_COMM_WORLD,&amp;my_rank);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span>(my_rank!=<span class="number">0</span>)&#123; </span><br><span class="line">        <span class="built_in">sprintf</span>(greeting,<span class="string">&quot;Greetings from process %d of %d!&quot;</span>,my_rank,comm_sz);</span><br><span class="line">        MPI_Send(greeting,<span class="built_in">strlen</span>(greeting)+<span class="number">1</span>,MPI_CHAR,<span class="number">0</span>,<span class="number">0</span>,MPI_COMM_WORLD);</span><br><span class="line">    &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;Greetings from process %d of %d!\n&quot;</span>,my_rank,comm_sz);</span><br><span class="line">        <span class="keyword">for</span>(<span class="keyword">int</span> q=<span class="number">1</span>;q&lt;comm_sz;q++)&#123;</span><br><span class="line">            MPI_Recv(greeting,MAX_STRING,MPI_CHAR,q,<span class="number">0</span>,MPI_COMM_WORLD,MPI_STATUS_IGNORE);</span><br><span class="line">            <span class="built_in">printf</span>(<span class="string">&quot;%s\n&quot;</span>,greeting);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    MPI_Finalize();</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<h4 id="1-2-MPI程序"><a href="#1-2-MPI程序" class="headerlink" title="1.2 MPI程序"></a>1.2 MPI程序</h4><p>​    C语言中使用MPI需要包含头文件mpi.h。头文件包括了MPI函数的原形、宏定义、类型定义等，它还包括了编译MPI程序需要的全部定义与声明。</p>
<p>​    所有MPI定义的标识符都由字符串MPI_开始。下划线后的第一个字母大写，表示函数名和MPI定义的类型。MPI定义的宏和常量的所有字母都是大写的。</p>
<h4 id="1-3-MPI-Init和MPI-Finalize"><a href="#1-3-MPI-Init和MPI-Finalize" class="headerlink" title="1.3 MPI_Init和MPI_Finalize"></a>1.3 MPI_Init和MPI_Finalize</h4><ul>
<li><p>调用<strong>MPI_Init</strong>是为了告知MPI系统进行所有必要的初始化设置。例如，系统可能需要为消息缓冲区分配存储空间，为进程指定进程号等。从经验上看，在程序调用MPI_Init前，不应该调用其他MPI函数。</p>
</li>
<li><p>MPI_Init的语法结构为：</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">MPI_Init</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">	<span class="keyword">int</span>* argc_p,</span></span></span><br><span class="line"><span class="params"><span class="function">	<span class="keyword">char</span>*** argv_p</span></span></span><br><span class="line"><span class="params"><span class="function">	)</span></span>;</span><br></pre></td></tr></table></figure>

<ul>
<li>参数argc_p和argv_p是指向参数argc和argv的指针。然而，当程序不使用这些参数时，可以只是将它们设置为NULL。</li>
<li>就像大部分MPI函数一样，MPI_Init返回一个int型错误码，在大部分情况下，我们忽略这些错误码。</li>
</ul>
</li>
<li><p>调用<strong>MPI_Finalize</strong>是为了告知MPI系统MPI已经使用完毕，为MPI而分配的任何资源都可以释放了。</p>
<ul>
<li>它的结构很简单：</li>
</ul>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">MPI_Finalize</span><span class="params">(<span class="keyword">void</span>)</span></span>;</span><br></pre></td></tr></table></figure>

<ul>
<li>一般而言，在调用MPI_Finalize后，就不应该调用MPI函数了。</li>
</ul>
</li>
<li><p>注意：我们比一定要向MPI_Init传递argc和argv的指针，也不一定要在main函数中调用MPI_Init和MPI_Finalize。</p>
</li>
</ul>
<h4 id="1-4-通信子、MPI-Comm-size和MPI-Comm-rank"><a href="#1-4-通信子、MPI-Comm-size和MPI-Comm-rank" class="headerlink" title="1.4 通信子、MPI_Comm_size和MPI_Comm_rank"></a>1.4 通信子、MPI_Comm_size和MPI_Comm_rank</h4><ul>
<li><p>在MPI中，<strong>通信子</strong>（communicator）指的是一组可以互相发送消息的进程集合。</p>
</li>
<li><p>MPI_Init的其中一个目的，是在用户启动程序时，定义由用户启动的所有进程所组成的通信子。这个通信子称为MPI_COMM_WORLD。</p>
</li>
<li><p>MPI_Comm_size和MPI_Comm_rank的语法结构为：</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">MPI_Comm_size</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">	MPI_Comm comm,  <span class="comment">//in</span></span></span></span><br><span class="line"><span class="params"><span class="function">	<span class="keyword">int</span>* comm_sz_p	<span class="comment">//out</span></span></span></span><br><span class="line"><span class="params"><span class="function">)</span></span>;</span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">MPI_Comm_rank</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">	MPI_Comm comm, <span class="comment">//in</span></span></span></span><br><span class="line"><span class="params"><span class="function">    <span class="keyword">int</span>* my_rank_p <span class="comment">//out</span></span></span></span><br><span class="line"><span class="params"><span class="function">)</span></span>;</span><br></pre></td></tr></table></figure></li>
<li><p>这两个函数中，第一个参数是一个通信子，它所属的类型是MPI为通信子定义的特殊类型：MPI_Comm。</p>
</li>
<li><p>MPI_Comm_size函数在它的第二个参数里返回通信子的进程数；MPI_Comm_rank函数在它的第二个参数里返回正在调用进程在通信子中的进程号。</p>
</li>
<li><p>在MPI_COMM_WORLD中经常用参数comm_sz表示进程的数量，用参数my_rank来表示进程号。</p>
</li>
</ul>
<h4 id="1-5-SPMD程序"><a href="#1-5-SPMD程序" class="headerlink" title="1.5 SPMD程序"></a>1.5 SPMD程序</h4><p>在并行编程中，0号进程常做的事是，当其他进程生成和发送消息时，它负责接受消息并打印出来。</p>
<p>事实上，大部分MPI程序都是这样的，编写一个单个程序，让不同进程产生不同动作。实现方式是，简单地让进程按照它们的进程号来匹配程序分支。这一方法称为单程序多数据流（Single Program，Multiple Data，SPMD）。</p>
<h4 id="1-6-MPI-Send"><a href="#1-6-MPI-Send" class="headerlink" title="1.6 MPI_Send"></a>1.6 MPI_Send</h4><p>每个消息的发送都是调用MPI_Send来实现的，其语法结构为：</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">MPI_Send</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">	<span class="keyword">void</span>* msg_buf_p,</span></span></span><br><span class="line"><span class="params"><span class="function">	<span class="keyword">int</span>  msg_size,</span></span></span><br><span class="line"><span class="params"><span class="function">	MPI_Datatype msg_type,</span></span></span><br><span class="line"><span class="params"><span class="function">	<span class="keyword">int</span> dest,</span></span></span><br><span class="line"><span class="params"><span class="function">	<span class="keyword">int</span> tag,</span></span></span><br><span class="line"><span class="params"><span class="function">	MPI_Comm communicator</span></span></span><br><span class="line"><span class="params"><span class="function">)</span></span>;</span><br></pre></td></tr></table></figure>

<ul>
<li><p>第一个参数msg_buf_p是一个指向包含消息内容的内存块的指针。</p>
</li>
<li><p>第二个参数msg_size是消息字符串的长度（总字符数量）</p>
</li>
<li><p>第三个参数msg_type是发送的数据类型，由于c语言中的类型（int，char等）不能作为参数传递给函数，所有MPI定义了一个特殊的类型：MPI_Datatype，用作参数msg_type，包括MPI_CHAR、MPI_SHORT、MPI_INT、MPI_LONG、MPI_LONG_LONG、MPI_UNSIGNED_CHAR、MPI_UNSIGNED_SHORT。</p>
</li>
<li><p>第四个参数dest指定了要接受消息的进程的进程号。</p>
</li>
<li><p>第五个参数tag是个非负int型，用于区分看上去完全一样的消息。例如，假设1号进程要向0号进程发送浮点数，其中一些要打印出来，而另一些要参与计算，为了区分，可以将用于打印的tag设为0，用于计算的tag设为1。</p>
<p>最后一个参数是一个通信子。所有涉及通信的MPI函数都有一个通信子参数。通信子最重要的目的之一是指定通信范围。通信子指的是一组可以互相发送消息的进程的集合。反过来，一个通信子中的进程所发送的消息不能被另一个通信子中的进程接收。由于MPI提供了创建新通信子的函数，因此通信子这一特性可以用于复杂程序，并保证消息不会意外地在错误的地方被接收。</p>
</li>
</ul>
<h4 id="1-7-MPI-Recv"><a href="#1-7-MPI-Recv" class="headerlink" title="1.7 MPI_Recv"></a>1.7 MPI_Recv</h4><p>MPI_Recv的前六个参数对应了MPI_Send的前六个参数：</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">MPI_Recv</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">	<span class="keyword">void</span>* msg_buf_p,</span></span></span><br><span class="line"><span class="params"><span class="function">	<span class="keyword">int</span> buf_size,</span></span></span><br><span class="line"><span class="params"><span class="function">	MPI_Datatype buf_type,</span></span></span><br><span class="line"><span class="params"><span class="function">	<span class="keyword">int</span> source,</span></span></span><br><span class="line"><span class="params"><span class="function">	<span class="keyword">int</span> tag,</span></span></span><br><span class="line"><span class="params"><span class="function">	MPI_Comm communicator,</span></span></span><br><span class="line"><span class="params"><span class="function">	MPI_Status* status_p</span></span></span><br><span class="line"><span class="params"><span class="function">)</span></span>;</span><br></pre></td></tr></table></figure>

<ul>
<li>前三个参数制定了用于<strong>接收消息</strong>的内存。后面三个参数用来识别消息。要与发送消息的进程相匹配。</li>
<li>最后一个参数status_p，在大部分情况下，调用函数并不使用这个参数，赋予其特殊的MPI常量MPI_STATUS_IGNORE就行。</li>
</ul>
<h4 id="1-8-消息匹配"><a href="#1-8-消息匹配" class="headerlink" title="1.8 消息匹配"></a>1.8 消息匹配</h4><ul>
<li><p>假定q号进程调用了MPI_Send函数：</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">MPI_Send</span>(send_buf_p,send_buf_sz,send_type,dest,send_tag,send_comm);</span><br></pre></td></tr></table></figure></li>
<li><p>并且假定r号进程调用了MPI_Recv函数：</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">MPI_Recv</span>(recv_buf_p,recv_buf_sz,recv_type,src,recv_tag,recv_comm,&amp;status);</span><br></pre></td></tr></table></figure></li>
<li><p>则q号进程调用MPI_Send函数所发送的消息可以被r号进程调用MPI_Recv函数接收，如果：</p>
<ul>
<li>recv_comm=send_comm,</li>
<li>recv_tag=send_tag,</li>
<li>dest=r并且src=q。</li>
<li><strong>如果recv_type=send_type，同时recv_buf_sz（接收缓冲）≥send_buf_sz（发送缓冲），那么由q号进程发送的消息就可以被r号进程成功地接收。</strong></li>
</ul>
</li>
<li><p>一个进程可以接收多个进程发来的消息，接收进程并不知道其他进程发送消息的顺序。如果分配给每个进程的工作所要耗费的实践是无法预测的，那么0号进程就无法知道其他进程完成工作的顺序。这样，如果按顺序接收各进程的结果，就会出现等待。为了避免这个问题，MPI提供了一个特殊的常量<strong>MPI_ANY_SOURCE</strong>，可以传递给MPI_Recv。这样0号进程执行下列代码，那么它可以按照进程完成工作的顺序来接收结果了：</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span>(i=<span class="number">1</span>;i&lt;comm_sz,i++)&#123;</span><br><span class="line">	<span class="built_in">MPI_Recv</span>(result,result_sz,result_type,MPI_ANY_SOURCE,result_tag,comm,</span><br><span class="line">		MPI_STATUS_IGNORE);</span><br><span class="line">	<span class="built_in">Process_result</span>(result);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li>
<li><p>类似地，一个进程也有可能接收多条来自另一个进程的有着不同标签的消息，并且接收进程并不知道消息发送的顺序。在这种情况下，MPI提供了特殊常量<strong>MPI_ANY_TAG</strong>，可以将它传给MPI_Recv的参数tag。</p>
</li>
<li><p>当使用这些“通配符（wildcard）参数时，有几点需要强调：</p>
<ul>
<li>1、<strong>只有接收者可以使用通配符参数。</strong>发送者必须指定一个进程号与一个非负整数标签。因此，MPI使用的是所谓的“推（push）“通信机制，而不是”拉（pull）“通信机制。</li>
<li>2、<strong>通信子参数没有通配符。发送者和接收者都必须指定通信子。</strong></li>
</ul>
</li>
</ul>
<h4 id="1-9-status-p参数"><a href="#1-9-status-p参数" class="headerlink" title="1.9 status_p参数"></a>1.9 status_p参数</h4><p>回想之前所说的规则，可以发现接收者可以在不知道以下信息的情况下接收消息：</p>
<p>1、消息中的数据量；</p>
<p>2、消息的发送者；</p>
<p>3、消息的标签</p>
<p>那么，接收者是如何找出这些值的？回想一下，MPI_Recv的最后一个参数的类型为<strong>MPI_Status</strong>*。MPI类型MPI_Status是一个有至少三个成员的结构，<strong>MPI_SOURCE、MPI_TAG和MPI_ERROR</strong>。</p>
<ul>
<li><p>假定程序含有如下的定义：<code>MPI_Status status;</code></p>
</li>
<li><p>那么将&amp;status作为最后一个参数传递给MPI_Recv函数并调用它后，可以通过检查以下两个成员来确定发送者和标签：</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">status.MPI_SOURCE</span><br><span class="line">status.MPI_TAG</span><br></pre></td></tr></table></figure></li>
<li><p>接收到的数据量不是存储在应用程序可以直接访问到的域中，但用户可以调用MPI_Get_count函数找回这个值。例如，假设对MPI_Recv的调用中，接收缓冲区的类型为recv_type，再次传递&amp;status参数，则以下调用</p>
<p><code>MPI_Get_count(&amp;status,recv_type,&amp;count)</code>会返回count参数接收到的元素数量。一般而言，MPI_Get_count的语法结构为：</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">MPI_Get_count</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">	MPI_Status* status_p,</span></span></span><br><span class="line"><span class="params"><span class="function">	MPI_Datatype type,</span></span></span><br><span class="line"><span class="params"><span class="function">	<span class="keyword">int</span>* count_p</span></span></span><br><span class="line"><span class="params"><span class="function">	)</span></span>;</span><br></pre></td></tr></table></figure>

<p>注意，count值并不能简单地作为MPI_Status变量的成员直接访问，因为它取决于接收数据的类型。因此，确定该值的过程需要一次计算（如接收到的字节数/每个对象的字节数）。如果这个信息不是必须的，那么我们没必要为了得到该值浪费一次计算。</p>
</li>
</ul>
<h4 id="1-10-MPI-Send和MPI-Recv的语义"><a href="#1-10-MPI-Send和MPI-Recv的语义" class="headerlink" title="1.10 MPI_Send和MPI_Recv的语义"></a>1.10 MPI_Send和MPI_Recv的语义</h4><p>​    当我们将消息从一个进程发送到另一个进程时，发生的许多细节取决于具体的系统，但我们可以有一些一般化的概念。<strong>发送进程组装信息</strong>，例如，它为实际要发送的数据添加“信封“信息。信息包括目标进程的进程号、发送进程的进程号、标签、通信子，以及消息大小等信息。一旦消息组装完毕，有两种可能性：<strong>发送进程可以缓冲消息，也可以阻塞（block）</strong>。如果它缓冲消息，则MPI系统将会把消息（包括数据和信封）放置在它自己的内部存储器里，并返回MPI_Send的调用。</p>
<p>​    另一方面，如果系统发生阻塞，那么它将一直等待，直到可以开始发送消息，并不立即返回对MPI_Send的调用。因此，如果使用MPI_Send，当函数返回时，实际上并不知道消息是否已经发送出去。我们之中你到消息所有的存储区，即发送缓冲区，可以被程序再次使用。如果我们需要直到消息是否已经发送出去，或者无论消息是否已经发送出去我们都让MPI_Send调用后立即返回，那么可以使用MPI提供的<strong>发送消息的替代方法</strong>。</p>
<p>​    MPI_Send的精确行为是由MPI实现所决定的。但是，典型的实现方法有一个<strong>默认的消息”截止“大小（”cutoff“message size）</strong>。如果一条消息的大小小于”截止“大小，它将被缓冲；如果大于截止大小，它将被阻塞。</p>
<p>​    与MPI_Send不同，<strong>MPI_Recv函数总是阻塞的，直到接收到一条匹配的消息</strong>。因此，当MPI_Recv函数调用返回时，就知道一条消息已经存储在接收缓冲区中了（除非产生了错误)。接收消息函数同样有<strong>替代函数</strong>，系统检查是否有一条匹配的消息并返回，而不管缓冲区中有没有消息。</p>
<p>​    MPI要求消息时<strong>不可超越的（nonovertaking）</strong>。即如果q号进程发送了两条消息给r号进程，那么q进程发送的第一条消息必须在第二条消息之前可用。但是，如果消息时来自不同进程的，消息的到达顺序是没有限制的。这本质上是因为MPI不能对网络的性能有强制要求。</p>
<h4 id="1-11-潜在的陷阱"><a href="#1-11-潜在的陷阱" class="headerlink" title="1.11 潜在的陷阱"></a>1.11 潜在的陷阱</h4><p>MPI_Recv的语义会导致MPI编程中的一个陷阱：如果一个进程试图接收消息，但没有相匹配的消息，那么该进程将会永远阻塞在那里，即<strong>进程悬挂</strong>。因此，在设计程序时，我们需要保证每条接收都有一条相匹配的发送。更重要的是，编写代码时，要格外小心以防止因调用MPI_Send和MPI_Recv出现<strong>匹配错误</strong>。</p>
<p>简单的说，如果调用MPI_Send发生了阻塞，并且没有相匹配的接收，那么发送进程就悬挂起来。另一方面，如果调用MPI_Send被缓冲，但没有相匹配的接收，那么消息将被丢失。</p>
</div><hr></div><nav id="pagination"><div class="pagination"><a class="extend prev" rel="prev" href="/"><i class="fa fa-chevron-left"></i></a><a class="page-number" href="/">1</a><span class="page-number current">2</span></div></nav></div></div><footer><div class="layout" id="footer"><div class="copyright">&copy;2013 - 2022 By LiYang</div><div class="framework-info"><span>Driven - </span><a target="_blank" rel="noopener" href="http://hexo.io"><span>Hexo</span></a><span class="footer-separator">|</span><span>Theme - </span><a target="_blank" rel="noopener" href="https://github.com/Molunerfinn/hexo-theme-melody"><span>Melody</span></a></div><div class="busuanzi"><script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script><span id="busuanzi_container_site_uv"><i class="fa fa-user"></i><span id="busuanzi_value_site_uv"></span><span></span></span><span class="footer-separator">|</span><span id="busuanzi_container_site_pv"><i class="fa fa-eye"></i><span id="busuanzi_value_site_pv"></span><span></span></span></div></div></footer><i class="fa fa-arrow-up" id="go-up" aria-hidden="true"></i><script src="https://cdn.jsdelivr.net/npm/animejs@latest/anime.min.js"></script><script src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"></script><script src="https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@latest/dist/jquery.fancybox.min.js"></script><script src="https://cdn.jsdelivr.net/npm/velocity-animate@latest/velocity.min.js"></script><script src="https://cdn.jsdelivr.net/npm/velocity-ui-pack@latest/velocity.ui.min.js"></script><script src="/js/utils.js?version=1.9.0"></script><script src="/js/fancybox.js?version=1.9.0"></script><script src="/js/sidebar.js?version=1.9.0"></script><script src="/js/copy.js?version=1.9.0"></script><script src="/js/fireworks.js?version=1.9.0"></script><script src="/js/transition.js?version=1.9.0"></script><script src="/js/scroll.js?version=1.9.0"></script><script src="/js/head.js?version=1.9.0"></script><script>if(/Android|webOS|iPhone|iPod|iPad|BlackBerry/i.test(navigator.userAgent)) {
  $('#nav').addClass('is-mobile')
  $('footer').addClass('is-mobile')
  $('#top-container').addClass('is-mobile')
}</script></body></html>